#include "scripting/scripting_engine.h"
#include <exception>
#include <filesystem>
#include <mutex>
#include <string>
#include <unordered_map>
#include <utils/singleton.hpp>
#include <vector>
#include "scripting/scripting_module.h"
#include "utils/log.hpp"
#include "utils/path_utils.hpp"

namespace fs = std::filesystem;

// 平台差异
#if _WIN32
#include "utils/win32_utility.hpp"

using LibHandle = HMODULE;

static inline void LibClose (LibHandle lib)
{
    FreeLibrary (lib);
}

#elif __unix__

#include <dlfcn.h>

using LibHandle = void *;

static inline void LibClose (LibHandle lib)
{
    dlclose (lib);
}

#else
#error "Not Supported!!"
#endif

class ScriptEngineData final : public PublicSingleton<ScriptEngineData>
{
    SINGLETON_TYPE ();

public:
    void LoadEngines ();
    void FreeEngines ();

    const ScriptModuleInfo *GetModule (const char *lang);

    struct ModuleInfoStruct {
        const ScriptModuleInfo *info;
        LibHandle               dll;
    };

    std::vector<const ScriptModuleInfo *>                   modules_;
    std::unordered_map<std::string, const ModuleInfoStruct> module_map_;

private:
    ScriptEngineData () {}
    ~ScriptEngineData () {}

    std::recursive_mutex lock_;
    using lock_type = std::lock_guard<decltype (lock_)>;
};

const ScriptModuleInfo **ScriptEngineList (int *count)
{
    auto &con = ScriptEngineData::Instance ().modules_;
    if (count)
        *count = (int) con.size ();
    return con.data ();
}

void ScriptEngineInitialize ()
{
    ScriptEngineData::Instance ().LoadEngines ();
}

void ScriptEngineShutdown ()
{
    ScriptEngineData::Instance ().FreeEngines ();
}

const ScriptModuleInfo *GetEngine (const char *lang)
{
    return ScriptEngineData::Instance ().GetModule (lang);
}

IScript *CreateScriptFromFile (const std::string &path)
{
    return nullptr;
}

static const ScriptModuleInfo *PlatformLoadScriptModule (const fs::path &path, LibHandle *dll);

void ScriptEngineData::LoadEngines ()
{
    lock_type lck (lock_);

    std::string modules_dir = "scripting"_AP;
    L_DEBUG ("modules dir is %s", modules_dir.c_str ());

    try
    {
        for (auto &file: fs::directory_iterator (modules_dir))
        {
            auto file_path = file.path ();
            auto extension = file_path.extension ().string ();
            auto path      = file_path.string ();
            auto stem      = file_path.stem ();
            if (extension == ".sm")
            {
                LibHandle handle;
                auto      mi = PlatformLoadScriptModule (file_path, &handle);
                if (mi)
                {
                    if (mi->init ())
                    {
                        modules_.emplace_back (mi);
                        module_map_.emplace (mi->lang, ModuleInfoStruct {mi, handle});
                    }
                    else
                    {
                        LibClose (handle);
                        L_ERROR ("Module %s Init Failed!", mi->name);
                    }
                }
            }
        }
    }
    catch (std::exception &e)
    {
        L_ERROR ("Load engines with exception: %s", e.what ());
    }
}

void ScriptEngineData::FreeEngines ()
{
    lock_type lck (lock_);

    for (auto &x: module_map_)
    {
        x.second.info->cleanup ();
        LibClose (x.second.dll);
    }

    modules_.clear ();
    module_map_.clear ();
}

const ScriptModuleInfo *ScriptEngineData::GetModule (const char *lang)
{
    const ScriptModuleInfo *result {};
    auto const              it = module_map_.find (lang);
    if (it != module_map_.end ())
    {
        result = it->second.info;
    }
    return result;
}

#if _WIN32

const ScriptModuleInfo *PlatformLoadScriptModule (const fs::path &path, LibHandle *dll)
{
    auto dir      = path.parent_path ();
    auto filename = path.filename ().replace_extension (".lib");
    auto libdir   = dir / filename;

    const ScriptModuleInfo *mi = nullptr;

    DLL_DIRECTORY_COOKIE cookie = nullptr;
    if (fs::is_directory (libdir))
    {
        auto wPath = libdir.wstring ();
        cookie     = AddDllDirectory (wPath.c_str ());
    }

    auto wDllPath = path.wstring ();
    if (const auto module = LoadLibraryExW (wDllPath.c_str (), NULL,
                                                    LOAD_LIBRARY_SEARCH_DEFAULT_DIRS |
                                           //LOAD_LIBRARY_SEARCH_USER_DIRS |
                                                    // LOAD_LIBRARY_SEARCH_APPLICATION_DIR |
                                                    // LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR |
                                                    //LOAD_LIBRARY_SEARCH_SYSTEM32 |
                                                    0))
    {
        if (const auto entry_point_func = reinterpret_cast<ModuleEntryFunc> (GetProcAddress (module, MODULE_ENTRY_NAME)))
        {
            if (mi = entry_point_func (); mi != nullptr)
            {
                *dll = module;
            }
            else
            {
                FreeLibrary (module);
            }
        }
        else
            FreeLibrary (module);
    }
    else
    {
        L_WARN("module %s load failed!", path.string().c_str());
    }
    if (cookie != nullptr)
        RemoveDllDirectory (cookie);

    return mi;
}
#elif __unix__

const ScriptModuleInfo *PlatformLoadScriptModule (const fs::path &path, LibHandle *dll)
{
    const ScriptModuleInfo *mi = nullptr;
    *dll                       = dlopen (path.string ().c_str (), RTLD_GLOBAL | RTLD_NOW);
    if (*dll)
    {
        if (const auto entry_point_func = reinterpret_cast<ModuleEntryFunc>(dlsym(*dll, MODULE_ENTRY_NAME)))
        {
            mi = entry_point_func();
            if (!mi)
            {
                dlclose(*dll);
                *dll = nullptr;
            }
        }
        else
        {
            L_WARN("Bad module file %s", path.string().c_str());
        }
    }
    else
    {
        L_WARN("Could not load %s: %s", path.string().c_str(), dlerror());
    }
    return mi;
}
#endif
